<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>调度结果可视化</title>
    <link rel="stylesheet" href="scheduleShow.css">
</head>
<body>
<div class="container">
    <div>
        <input type="file" id="file-input"/>
        <button id="show-all">展示全部工序</button>
        <button id="show-animation">顺序加工动画</button>
    </div>
    <div class="canvas-container">
        <canvas id="paint-canvas"></canvas>
    </div>
</div>

<script>
    var animation;
    var showAnimationButton=document.getElementById("show-animation");
    var showAnimationHandler=function (){};
    var showAllHandler=function (){};
    var showAllButton=document.getElementById("show-all");

    document.getElementById('file-input').addEventListener('change', (event) => {
        clearInterval(animation);
        showAnimationButton.removeEventListener("mousedown",showAnimationHandler);
        showAllButton.removeEventListener("mousedown",showAllHandler);
        readTxt(event).then((machines) => {
            draw(25, 50, 21, 30, 5, 5, machines);
        });
    });

    /**
     * h5读取选中的文件的内容，由于reader.onload是异步的，所以需要使用new Promise((resolve, reject)进行包装
     * @param event
     * @returns {Promise<unknown>}
     * @author xcy
     */
    function readTxt(event) {
        return new Promise((resolve, reject) => {
            const file = event.target.files[0];
            const reader = new FileReader();
            reader.onload = (event) => {
                const content = event.target.result;
                let machines = [];
                let machineStringArr = content.split("|")
                //给每一个作业一个颜色
                let map = new Map();
                machineStringArr.forEach(machineString => {
                    let machine = []
                    if (machineString !== "") {
                        let operationStringArr = machineString.substring(1, machineString.length - 1).split(")(");
                        operationStringArr.forEach(operationString => {
                            let operationDataArr = operationString.split(",");
                            let jobNumber = operationDataArr[0].split("-")[0];
                            let r = 140 + 115 * Math.random(), g = 140 + 115 * Math.random(),
                                b = 140 + 115 * Math.random();
                            if (!map.has(jobNumber)) {
                                map.set(jobNumber, `rgba(${r},${g},${b},${0.8})`);
                            }
                            machine.push({
                                code: operationDataArr[0],
                                machineNumber:machines.length,
                                releaseTime: parseInt(operationDataArr[1]),
                                completeTime: parseInt(operationDataArr[2]),
                                color: map.get(jobNumber)
                            })
                        })
                    }
                    machines.push(machine);
                })
                resolve(machines);
            };
            reader.onerror = (error) => {
                reject(error);
            };
            reader.readAsText(file);
        })
    }

    //甘特图首行高度，首列宽度，单位时间的宽度，行高，相邻两行的间距，边框间距，调度计划
    function draw(firstRowHeight, firstColWidth, perTimeWidth, rowHeight, rowSpacing, bordSpacing, machines) {
        let maxCompleteTime = 0;
        let minDuration = Number.MAX_VALUE;
        machines.forEach(machine => {
            machine.forEach(operation => {
                minDuration = Math.min(operation.completeTime - operation.releaseTime)
            })
            if (machine.length !== 0) {
                let completeTime = machine[machine.length - 1].completeTime;
                maxCompleteTime = Math.max(maxCompleteTime, completeTime);
            }
        })

        if (firstColWidth < 70) {
            firstColWidth = 70;
        }
        //调度表宽度和调度表高度
        let scheduleHeight = firstRowHeight + rowSpacing + machines.length * (rowSpacing + rowHeight);
        let scheduleWidth = firstColWidth + perTimeWidth * maxCompleteTime;

        let canvasContainer = document.getElementsByClassName("canvas-container")[0];
        canvasContainer.maxWidth = window.innerWidth


        //获得画布对象
        let canvas = document.getElementById("paint-canvas");
        canvas.width = scheduleWidth + bordSpacing * 2;
        canvas.height = scheduleHeight + bordSpacing * 2;
        if (!canvas.getContext("2d")) {
            return;
        }
        let ctx = canvas.getContext("2d");
        let fontSize = 12
        ctx.font = `${fontSize}px serif`;
        ctx.textAlign = 'center';

        //画布的背景颜色
        ctx.save();
        ctx.fillStyle = "#ffffff";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.restore();

        //画出调度表的边框，画出首行和首列
        let currentX = bordSpacing;
        let currentY = bordSpacing;
        drawBorder(ctx,currentX,currentY,scheduleWidth,scheduleHeight,firstColWidth,firstRowHeight);


        currentX = bordSpacing + firstColWidth + perTimeWidth;
        currentY = bordSpacing + firstRowHeight;
        drawCol(ctx,maxCompleteTime,currentX,currentY,rowHeight,rowSpacing,machines,perTimeWidth);

        //画出首行之后的各行
        currentX = bordSpacing;
        currentY = bordSpacing + firstRowHeight + rowSpacing;
        drawRow(ctx,machines,currentX,currentY,scheduleWidth,firstColWidth,rowHeight,rowSpacing,fontSize);

        //监听按钮点击画出调度表的所有工序
        showAllHandler=function (){
            drawOperations(currentX,currentY,perTimeWidth,rowHeight,fontSize,rowSpacing,ctx,machines,false,"");
        }
        currentX = bordSpacing + firstColWidth;
        currentY = bordSpacing + firstRowHeight + rowSpacing;
        showAllButton.addEventListener("mousedown",showAllHandler);

        //监听按钮点击画出动画
        showAnimationHandler=function (){
            drawAnimation(machines,firstColWidth,scheduleWidth,scheduleHeight,bordSpacing,perTimeWidth,
                currentX,currentY,rowHeight,firstRowHeight,maxCompleteTime,fontSize,rowSpacing,ctx);
        }
        showAnimationButton.addEventListener("mousedown",showAnimationHandler)

        currentX = bordSpacing + firstColWidth;
        currentY = bordSpacing + firstRowHeight + rowSpacing;
        //监听鼠标点击
        canvas.addEventListener("mousedown",(event)=>{
            drawFocus(rowHeight,perTimeWidth,rowSpacing,fontSize,currentX,currentY,event.offsetX,event.offsetY,
                machines,ctx,scheduleHeight,scheduleWidth,firstRowHeight,firstColWidth,bordSpacing,maxCompleteTime);
        })
    }

    //画出边框
    function drawBorder(ctx,currentX,currentY,scheduleWidth,scheduleHeight,firstColWidth,firstRowHeight){
        ctx.save();
        ctx.fillStyle = "#ececec";
        ctx.strokeStyle = "#000000"
        ctx.lineWidth = 2;
        ctx.strokeRect(currentX, currentY, scheduleWidth, scheduleHeight);
        ctx.fillRect(currentX, currentY, scheduleWidth, firstRowHeight);
        ctx.fillRect(currentX, currentY, firstColWidth, scheduleHeight);
        ctx.strokeRect(currentX, currentY, scheduleWidth, firstRowHeight);
        ctx.strokeRect(currentX, currentY, firstColWidth, scheduleHeight);
        ctx.restore();
    }

    //画出首行之后的各行
    function drawRow(ctx,machines,currentX,currentY,scheduleWidth,firstColWidth,rowHeight,rowSpacing,fontSize){
        ctx.beginPath();
        ctx.strokeStyle = "rgba(170,170,170,0.34)"
        for (let i = 0; i < machines.length; i++) {
            ctx.fillText(`机器${i}`, currentX + firstColWidth / 2.0, currentY + rowHeight / 2 + fontSize / 2);
            ctx.moveTo(currentX, currentY);
            ctx.lineTo(currentX + scheduleWidth, currentY);
            ctx.stroke();
            ctx.moveTo(currentX, currentY + rowHeight);
            ctx.lineTo(currentX + scheduleWidth, currentY + rowHeight);
            ctx.stroke();
            currentY += rowHeight + rowSpacing;
        }
    }

    //画出首列后面的各列
    function drawCol(ctx,maxCompleteTime,currentX,currentY,rowHeight,rowSpacing,machines,perTimeWidth){

        ctx.save();
        ctx.beginPath();
        ctx.lineWidth = 1;
        ctx.strokeStyle = "#bbbbbb";
        ctx.setLineDash([7.5, 5]);
        for (let i = 1; i < maxCompleteTime; i++) {
            if (i % 4 === 0) {
                ctx.fillText(`${i}`, currentX, currentY - 5)
                ctx.moveTo(currentX, currentY);
                ctx.lineTo(currentX, currentY + rowSpacing + machines.length * (rowSpacing + rowHeight));
                ctx.stroke();
            }
            currentX += perTimeWidth;
        }
        ctx.restore();
    }

    //画出工序的颜色、位置、持续时间
    function drawOperations(currentX,currentY,perTimeWidth,rowHeight,
                            fontSize,rowSpacing,ctx,machines,
                            isFocus,focusOpr){
        clearInterval(animation);
        for (let i = 0; i < machines.length; i++) {
            let machine = machines[i];
            for (let j = 0; j < machine.length; j++) {
                let operation = machine[j];
                let completeTime = operation.completeTime
                let releaseTime = operation.releaseTime
                let duration = (completeTime - releaseTime) * perTimeWidth;

                ctx.save();

                if(isFocus&&focusOpr!==""){
                    let focusJobNumber=focusOpr.split("-")[0];
                    let jobNumber=operation.code.split("-")[0];
                    if(focusJobNumber===jobNumber){
                        ctx.fillStyle = operation.color;
                    }else {
                        ctx.fillStyle = "rgba(182,182,182,0.2)";
                    }
                }else {
                    ctx.fillStyle = operation.color;
                }
                ctx.strokeStyle = "#000000"
                ctx.fillRect(currentX + releaseTime * perTimeWidth, currentY, duration, rowHeight);
                ctx.strokeRect(currentX + releaseTime * perTimeWidth, currentY, duration, rowHeight);
                ctx.restore();
                ctx.fillText(operation.code, currentX + releaseTime * perTimeWidth + duration / 2, currentY + rowHeight / 2 + fontSize / 2)
            }
            //更新当前y坐标
            currentY += rowHeight + rowSpacing;
        }
    }

    //点击某一道工序，使这个工序所属的作业保留颜色，其他作业的颜色变成黑白
    function drawFocus(rowHeight,colWidth,rowSpacing,fontSize,
                       currentX,currentY,mouseX,mouseY,machines,ctx,
                       scheduleHeight,scheduleWidth,firstRowHeight,
                       firstColWidth,borderSpacing,maxCompleteTime){
        if(mouseX<currentX||mouseY<currentY){
            return;
        }
        //识别出当前点击的工序   首先识别点击了哪个机器
        let machineNumber=Math.floor((mouseY-currentY)/(rowSpacing+rowHeight));
        let time=Math.floor((mouseX-currentX)/colWidth);
        if(machineNumber>=machines.length){
            return;
        }

        ctx.clearRect(0,0,scheduleWidth+100,scheduleHeight+100);

        currentX = borderSpacing;
        currentY = borderSpacing;
        drawBorder(ctx,currentX,currentY,scheduleWidth,scheduleHeight,firstColWidth,firstRowHeight);

        currentX = borderSpacing + firstColWidth + colWidth;
        currentY = borderSpacing + firstRowHeight;
        drawCol(ctx,maxCompleteTime,currentX,currentY,rowHeight,rowSpacing,machines,colWidth);

        currentX = borderSpacing;
        currentY = borderSpacing + firstRowHeight + rowSpacing;
        drawRow(ctx,machines,currentX,currentY,scheduleWidth,firstColWidth,rowHeight,rowSpacing,fontSize);

        currentX = borderSpacing + firstColWidth;
        currentY = borderSpacing + firstRowHeight + rowSpacing;
        let machine=machines[machineNumber];
        let focusOpr="";
        machine.forEach(operation=>{
            if(operation.releaseTime<=time&&time<=operation.completeTime){
                focusOpr=operation.code;
            }
        })
        drawOperations(currentX,currentY,colWidth,rowHeight,fontSize,rowSpacing,ctx,machines,true,focusOpr);
    }

    function drawAnimation(machines,firstColWidth,scheduleWidth,scheduleHeight,borderSpacing,
                           colWidth,currentX,currentY,rowHeight,firstRowHeight,
                           maxCompleteTime,fontSize,rowSpacing,ctx){
        let cloneOperations=JSON.parse(JSON.stringify(getOperationsSortedByStartTime(machines)));
        //把甘特图清空一下
        ctx.clearRect(0,0,scheduleWidth+100,scheduleHeight+100);
        currentX = borderSpacing;
        currentY = borderSpacing;
        drawBorder(ctx,currentX,currentY,scheduleWidth,scheduleHeight,firstColWidth,firstRowHeight);
        currentX = borderSpacing + firstColWidth + colWidth;
        currentY = borderSpacing + firstRowHeight;
        drawCol(ctx,maxCompleteTime,currentX,currentY,rowHeight,rowSpacing,machines,colWidth);
        currentX = borderSpacing;
        currentY = borderSpacing + firstRowHeight + rowSpacing;
        drawRow(ctx,machines,currentX,currentY,scheduleWidth,firstColWidth,rowHeight,rowSpacing,fontSize);

        currentX = borderSpacing + firstColWidth;
        animation = setInterval(() => {
            if (cloneOperations.length === 0) {
                return;
            }
            let operation = cloneOperations[0];
            let completeTime = operation.completeTime;
            let releaseTime = operation.releaseTime;
            let duration = (completeTime - releaseTime) * colWidth;
            let operationY = currentY + operation.machineNumber * (rowHeight + rowSpacing);
            ctx.save();
            ctx.fillStyle = operation.color;
            ctx.strokeStyle = "#000000";
            ctx.fillRect(currentX + releaseTime * colWidth, operationY, duration, rowHeight);
            ctx.strokeRect(currentX + releaseTime * colWidth, operationY, duration, rowHeight);
            ctx.restore();
            ctx.fillText(operation.code, currentX + releaseTime * colWidth + duration / 2, operationY + rowHeight / 2 + fontSize / 2)
            cloneOperations.shift()
        }, 250);
    }

    //从machines中获得按照释放时间排序的工序列表
    function getOperationsSortedByStartTime(machines){
        let sortedOperations=[];
        machines.forEach(machine=>{
            machine.forEach(operation=>sortedOperations.push(operation))
        })
        sortedOperations.sort((o1,o2)=>{return o1.releaseTime-o2.releaseTime});
        return sortedOperations;
    }

</script>

</body>
</html>
